﻿// <copyright file="Connector.cs" company="The Amuse project">
//     Copyright © 2009, Nidonocu and other Contributors. All rights reserved.
//     Licensed under the BSD License - See 'Amuse License.txt' for full details.
// </copyright>

namespace Nox.Amuse.Net
{
    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Text;
    using System.Net;
    using System.Net.Sockets;
    using Org.Mentalis.Network.ProxySocket;
    using System.Diagnostics;
    using System.Net.Security;
    using System.Security.Cryptography.X509Certificates;
    using System.Security.Authentication;
    using System.IO;
    using System.Globalization;

    /// <summary>
    /// Manages the connection of a session
    /// </summary>
    public static class Connector
    {
        /// <summary>
        /// Performs a DNSQuery and returns the results
        /// </summary>
        /// <param name="hostName">The hostName to look up</param>
        /// <returns>DnsQueryResult with result of the query and addresses if found</returns>
        public static DnsQueryResult QueryDns(string hostName)
        {
            try
            {
                IPAddress[] addresses = Dns.GetHostAddresses(hostName);
                return new DnsQueryResult { Addresses = addresses, Error = DnsErrorType.None };
            }
            catch (ArgumentException)
            {
                return new DnsQueryResult { Error = DnsErrorType.IPAddressInvalid };
            }
            catch (SocketException fault)
            {
                if (fault.SocketErrorCode == SocketError.HostNotFound)
                    return new DnsQueryResult { Error = DnsErrorType.HostNotFound };
                else
                    return new DnsQueryResult { Error = DnsErrorType.DnsProblem };
            }
        }

        #region Overloads
        /// <summary>
        /// Connects to a game server and returns a set of objects for working with the new connection (Standard with No Proxy)
        /// </summary>
        /// <param name="target">IPEndPoint containing target IP address and Port</param>
        /// <param name="type">Type of Connection to establish</param>
        /// <returns>A ConnectionResult object containing a TcpClient and Stream or Error Details</returns>
        public static ConnectionResult ConnectToServer(IPEndPoint target, ConnectionOptions type)
        {
            return ConnectToServer(target, type, null, null, null);
        }
        /// <summary>
        /// Connects to a game server and returns a set of objects for working with the new connection (Secure with No Proxy)
        /// </summary>
        /// <param name="target">IPEndPoint containing target IP address and Port</param>
        /// <param name="type">Type of Connection to establish</param>
        /// <param name="hostName">The hostName to be used when validating a server's SSL certificate</param>
        /// <param name="callback">Delegate for a method to provide user feedback on certificate problems</param>
        /// <returns>A ConnectionResult object containing a TcpClient and Stream or Error Details</returns>
        public static ConnectionResult ConnectToServer(IPEndPoint target, ConnectionOptions type, string hostName, CertificateProblemUIHandler callback)
        {
            return ConnectToServer(target, type, hostName, callback, null);
        }
        #endregion
        /// <summary>
        /// Connects to a game server and returns a set of objects for working with the new connection (Standard or Secure with Proxy)
        /// </summary>
        /// <param name="target">IPEndPoint containing target IP address and Port</param>
        /// <param name="type">Type of Connection to establish</param>
        /// <param name="hostName">The hostName to be used when validating a server's SSL certificate</param>
        /// <param name="callback">Delegate for a method to provide user feedback on certificate problems</param>
        /// <param name="proxyTarget">IPEndPoint containing the proxy server's IP address and Port</param>
        /// <returns>A ConnectionResult object containing a TcpClient and Stream or Error Details</returns>
        public static ConnectionResult ConnectToServer(IPEndPoint target, ConnectionOptions type, string hostName, CertificateProblemUIHandler callback, IPEndPoint proxyTarget)
        {
            var TcpLink = new TcpClient(target.AddressFamily);

            if ((type & ConnectionOptions.Proxy) == ConnectionOptions.Proxy)
            {
                if (proxyTarget == null)
                    throw new ArgumentException("Proxy Target IP must be provided");

                //Begin Proxy Connection
                try
                {
                    var proxy = new ProxySocket(AddressFamily.InterNetwork,
                        SocketType.Stream, ProtocolType.Tcp);
                    proxy.ProxyEndPoint = proxyTarget;
                    proxy.ProxyType = ProxyTypes.Socks4;	//TODO: Allow configuration
                    // (Note that the proxy server will resolve the domain name for us)
                    proxy.Connect(target);
                    TcpLink.Client = proxy;
                }
                catch (ProxyException proxyFault)
                {
                    Debug.Print("Proxy Exception: " + proxyFault.Message);
                    return new ConnectionResult()
                    {
                        Error = ConnectionErrorTypes.ProxyFailure,
                        ErrorMessage = proxyFault.Message
                    };
                }
                catch (SocketException socketFault)
                {
                    Debug.Print("Socket Exception: " + socketFault.Message);
                    return new ConnectionResult()
                    {
                        Error = IdentifySocketError(socketFault),
                        ErrorMessage = socketFault.Message
                    };
                }
            }
            else
            {
                try
                {
                    Debug.Print("Connecting to " + target.ToString());
                    TcpLink.Connect(target);
                    Debug.Print("Connection Request sent");
                }
                catch (SocketException sockFault)
                {
                    Debug.Print("Socket Exception: " + sockFault.Message);
                    return new ConnectionResult()
                    {
                        Error = IdentifySocketError(sockFault),
                        ErrorMessage = sockFault.Message
                    };
                }
            }

            if ((type & ConnectionOptions.Secure) == ConnectionOptions.Secure)
            {
                if (string.IsNullOrEmpty(hostName))
                    throw new ArgumentException("Hostname for secure match must be provided");
                if (callback == null)
                    throw new ArgumentException("Certificate Problem UI Delegate must be provided");

                SslStream SecureStream = null;
                try
                {
                    Debug.Print("Creating SSL Stream");
                    SecureStream = new SslStream(TcpLink.GetStream(), false,
                        delegate(
                        object sender,
                        X509Certificate certificate,
                        X509Chain chain,
                        SslPolicyErrors sslPolicyErrors)
                        {
                            Console.WriteLine("Validating");
                            Console.WriteLine(sslPolicyErrors);
                            if ((sslPolicyErrors & SslPolicyErrors.RemoteCertificateNotAvailable) ==
                                SslPolicyErrors.RemoteCertificateNotAvailable)
                            {
                                Debug.Print("No Certificate");
                                callback(new CertificateProblemInformation()
                                {
                                    Error = AuthenticationErrorType.RemoteCertificateNotAvailable,
                                    HostName = hostName
                                });
                                return false;
                            }
                            if ((sslPolicyErrors & SslPolicyErrors.RemoteCertificateNameMismatch) ==
                                SslPolicyErrors.RemoteCertificateNameMismatch)
                            {
                                Console.WriteLine("Name Mismatch");
                                return callback(new CertificateProblemInformation()
                                {
                                    Error = AuthenticationErrorType.RemoteCertificateNameMismatch,
                                    HostName = hostName,
                                    CertificateSubject = certificate.ToString(),
                                    Certificate = certificate
                                }); //TODO: Look in to tidying output
                            }
                            if (DateTime.Now >
                                DateTime.Parse(certificate.GetExpirationDateString(), CultureInfo.InvariantCulture))
                            {
                                Debug.Print("Out Of Time - Expired");
                                return callback(new CertificateProblemInformation()
                                {
                                    Error = AuthenticationErrorType.RemoteCertificateOutOfTime,
                                    HostName = hostName,
                                    CertificateSubject = certificate.ToString(),
                                    Certificate = certificate,
                                    ProblemDate = DateTime.Parse(certificate.GetExpirationDateString(), CultureInfo.InvariantCulture),
                                    ProblemDateIsStartDate = false
                                });
                            }
                            if (DateTime.Parse(certificate.GetEffectiveDateString(), CultureInfo.InvariantCulture) >
                                DateTime.Now)
                            {
                                Debug.Print("Out Of Time - Early");
                                return callback(new CertificateProblemInformation()
                                {
                                    Error = AuthenticationErrorType.RemoteCertificateOutOfTime,
                                    HostName = hostName,
                                    CertificateSubject = certificate.ToString(),
                                    Certificate = certificate,
                                    ProblemDate = DateTime.Parse(
                                        certificate.GetEffectiveDateString(),
                                        CultureInfo.InvariantCulture),
                                    ProblemDateIsStartDate = true
                                });
                            }
                            else
                                return true;
                        });
                    Debug.Print("Accessing Store");
                    var store = new X509Store(StoreName.Root, StoreLocation.CurrentUser);
                    store.Open(OpenFlags.ReadOnly | OpenFlags.OpenExistingOnly);
                    var collection = (X509Certificate2Collection)store.Certificates;
                    Debug.Print("Authenticating as client");
                    SecureStream.AuthenticateAsClient(hostName,
                        (X509CertificateCollection)collection, SslProtocols.Tls, true);

                    return new ConnectionResult()
                    {
                        Error = ConnectionErrorTypes.None,
                        Client = TcpLink,
                        DataStream = SecureStream,
                        StreamType = typeof(SslStream)
                    };
                }
                catch (SocketException socketFault)
                {
                    Debug.Print("Error trying to connect: " + socketFault.Message);
                    return new ConnectionResult()
                    {
                        Error = IdentifySocketError(socketFault),
                        ErrorMessage = socketFault.Message
                    };
                }
                catch (AuthenticationException authFault)
                {
                    Debug.Print("Error trying to connect: " + authFault.Message);
                    return new ConnectionResult()
                    {
                        Error = ConnectionErrorTypes.DidNotAuthenticate,
                        ErrorMessage = authFault.Message
                    };
                }
            }

            //Return Standard
            try
            {
                return new ConnectionResult()
                {
                    Error = ConnectionErrorTypes.None,
                    Client = TcpLink,
                    DataStream = TcpLink.GetStream(),
                    StreamType = typeof(NetworkStream)
                };
            }
            catch (SocketException socketFault)
            {
                Debug.Print("Error trying to connect: " + socketFault.Message);
                return new ConnectionResult()
                {
                    Error = IdentifySocketError(socketFault),
                    ErrorMessage = socketFault.Message
                };
            }
        }

        /// <summary>
        /// Returns a simplifed ConnectionErrorType
        /// </summary>
        /// <param name="fault">The SocketException that occured</param>
        /// <returns>The ConnectionErrorType</returns>
        public static ConnectionErrorTypes IdentifySocketError(SocketException fault)
        {
            switch (fault.SocketErrorCode)
            {
                case SocketError.ConnectionRefused:
                    return ConnectionErrorTypes.ServerRefused;
                case SocketError.HostUnreachable:
                    return ConnectionErrorTypes.UnableToReachHost;
                case SocketError.TimedOut:
                    return ConnectionErrorTypes.TimedOut;
                case SocketError.ConnectionReset:
                    return ConnectionErrorTypes.ConnectionReset;
                case SocketError.NoBufferSpaceAvailable:
                    return ConnectionErrorTypes.SystemOverloaded;
                case SocketError.ProcessLimit:
                    return ConnectionErrorTypes.SystemOverloaded;
                case SocketError.TooManyOpenSockets:
                    return ConnectionErrorTypes.SystemOverloaded;
                default:
                    return ConnectionErrorTypes.Broken;
            }
        }
    }

    /// <summary>Enumerates possible types of DNS error</summary>
    public enum DnsErrorType
    {
        /// <summary>
        /// No error occured while accessing the DNS
        /// </summary>
        None,
        /// <summary>
        /// The IP Address (typed by the user or supplied by the server) is invalid
        /// </summary>
        IPAddressInvalid,
        /// <summary>
        /// Either the webserver does not exist or no connection could be made with the DNS Server
        /// </summary>
        HostNotFound,
        /// <summary>
        /// The DnsQuery was never started due to a local problem beginning the query
        /// </summary>
        CouldNotStart,
        /// <summary>
        /// There was a some other problem while trying to connect to the DNS Server
        /// </summary>
        DnsProblem
    }

    /// <summary>
    /// Defines the results of a DNS Query
    /// </summary>
    public sealed class DnsQueryResult
    {
        /// <summary>
        /// Defines the type of Error or OK result
        /// </summary>
        public DnsErrorType Error { get; set; }

        /// <summary>
        /// Defines the returned addresses
        /// </summary>
        public IPAddress[] Addresses { get; set; }
    }

    /// <summary>Enumerates options that can be applied to connections</summary>
    [Flags]
    public enum ConnectionOptions
    {
        /// <summary>
        /// A regular connection using .net Sockets only
        /// </summary>
        None = 0,
        /// <summary>
        /// A SSL connection using the connection libraries
        /// </summary>
        Secure = 1,
        /// <summary>
        /// A Proxied connection using the connection libraries
        /// </summary>
        Proxy = 2
    }

    /// <summary>Enumerates possible types of Connection error</summary>
    [Flags]
    public enum ConnectionErrorTypes
    {
        /// <summary>
        /// No occured while connecting
        /// </summary>
        None = 0,
        /// <summary>
        /// An error occured while the socket was being created and started
        /// </summary>
        DidNotStart = 1,
        /// <summary>
        /// A serious and uexpected socket error occured
        /// </summary>
        Broken = 2,
        /// <summary>
        /// Too many sockets are in use or other system resources are overloaded
        /// </summary>
        SystemOverloaded = 4,
        /// <summary>
        /// Placeholder for reset
        /// </summary>
        ConnectionReset = 8, //TODO: Find out if this happens on hard disconnect
        /// <summary>
        /// The target server could be reached via any network, the server or the user's computer is not connected to the internet
        /// </summary>
        UnableToReachHost = 16,
        /// <summary>
        /// No response came during a fair amount of time from the target server, game software is likey not running
        /// </summary>
        TimedOut = 32,
        /// <summary>
        /// Server is there but the game software did not take the connection request
        /// </summary>
        ServerRefused = 64,
        /// <summary>
        /// The user refused authentication for the secure server
        /// </summary>
        DidNotAuthenticate = 128,
        /// <summary>
        /// A connection could not be made to the proxy server
        /// </summary>
        ProxyFailure = 256
    }

    /// <summary>
    /// Defines the results of a Connection Attempt
    /// </summary>
    public sealed class ConnectionResult
    {
        /// <summary>
        /// Defines the type of Error or OK result
        /// </summary>
        public ConnectionErrorTypes Error { get; set; }
        /// <summary>
        /// String containing error message as received from framework
        /// </summary>
        public string ErrorMessage { get; set; }

        /// <summary>
        /// TcpClient containing the control system for the connection
        /// </summary>
        public TcpClient Client { get; set; }
        /// <summary>
        /// Strean containing access to data being transfered
        /// </summary>
        public Stream DataStream { get; set; }
        /// <summary>
        /// Type of stream being used
        /// </summary>
        public Type StreamType { get; set; }
    }
    /// <summary>
    /// Enumerates possible types of Authentication error
    /// </summary>
    /// <remarks>This enum is created to avoid needing a System.Net.Security call in calling code</remarks>
    public enum AuthenticationErrorType
    {
        /// <summary>
        /// No problem was found the certificate
        /// </summary>
        NoAuthenticationError,
        /// <summary>
        /// The name of the certificate does not match the server name
        /// </summary>
        RemoteCertificateNameMismatch,
        /// <summary>
        /// The current date does not fall after the issue date or before the expiry date of the certificate
        /// </summary>
        RemoteCertificateOutOfTime,
        /// <summary>
        /// The certificate is unavalible
        /// </summary>
        RemoteCertificateNotAvailable
    }

    /// <summary>
    /// Defines a problem when a Certificate has failed
    /// </summary>
    public sealed class CertificateProblemInformation
    {
        /// <summary>
        /// Stores the type of problem with the certificate
        /// </summary>
        public AuthenticationErrorType Error { get; set; }
        /// <summary>
        /// Stores the hostname provided by the user for the server
        /// </summary>
        public string HostName { get; set; }
        /// <summary>
        /// Stores the data provided by the certificate
        /// </summary>
        public string CertificateSubject { get; set; }
        /// <summary>
        /// Stores the issue or expiry date that is on the wrong side of the current date
        /// </summary>
        public DateTime ProblemDate { get; set; }
        /// <summary>
        /// If true, the date stored in ProblemDate is the Issue date. If false, the date stored in ProblemDate is the Expiry date.
        /// </summary>
        public bool ProblemDateIsStartDate { get; set; }
        /// <summary>
        /// Stores the certificate for viewing the complete details of
        /// </summary>
        public X509Certificate Certificate { get; set; }
        /// <summary>
        /// Displays a property sheet with the full details of the certificate
        /// </summary>
        public void DisplayDetailedView()
        {
            if (Certificate != null)
                X509Certificate2UI.DisplayCertificate((X509Certificate2)Certificate);
        }
    }

    /// <summary>
    /// Delegate for methods to provide the user with information about and the ability to provide feedback on certificate problems on SSL connections
    /// </summary>
    /// <param name="info">Data defining the problem</param>
    /// <returns>Bool to be set true if the user approves the certificate, false if the connection should be aborted.</returns>
    public delegate bool CertificateProblemUIHandler(CertificateProblemInformation info);
}
